﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using Hive.Core.Common.Collections;
using Hive.Core.Model.Schema;

namespace Hive.Core.Model
{
    /// <summary>
    /// A descriptor is a hierarchy of string property key/value pairs that can be
    /// loaded and saved as a string.
    /// </summary>
    public class Descriptor
    {
        private readonly Dictionary<string, string> properties;
        private readonly List<KeyValuePair<string, Descriptor>> children;

        /// <summary>
        /// Creates an empty descriptor.
        /// </summary>
        public Descriptor()
        {
            properties = new Dictionary<string, string>();
            children = new List<KeyValuePair<string, Descriptor>>();
        }

        /// <summary>
        /// Gets the read-only dictionary of properties.
        /// </summary>
        public IDictionary<string, string> Properties
        {
            get { return new ReadOnlyDictionary<string, string>(properties); }
        }

        /// <summary>
        /// Gets the read-only collection of child descriptors.
        /// </summary>
        public ICollection<KeyValuePair<string, Descriptor>> Children
        {
            get { return new ReadOnlyCollection<KeyValuePair<string, Descriptor>>(children); }
        }

        /// <summary>
        /// Adds a property.
        /// </summary>
        /// <param name="key">The property key.</param>
        /// <param name="value">The property value.</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> or <paramref name="value"/> is null.</exception>
        /// <exception cref="InvalidOperationException">Thrown if <paramref name="key"/> is already defined.</exception>
        public void AddProperty(string key, string value)
        {
            if (key == null)
                throw new ArgumentNullException("key");
            if (value == null)
                throw new ArgumentNullException("value");

            properties.Add(key, value);
        }

        /// <summary>
        /// Removes a property.
        /// </summary>
        /// <param name="key">The property key.</param>
        /// <returns>True if the property previously existed and was removed.</returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is null.</exception>
        public bool RemoveProperty(string key)
        {
            if (key == null)
                throw new ArgumentNullException("key");

            return properties.Remove(key);
        }

        /// <summary>
        /// Removes all properties.
        /// </summary>
        public void ClearProperties()
        {
            properties.Clear();
        }

        /// <summary>
        /// Gets the value of a property or null if it does not exist.
        /// </summary>
        /// <param name="key">The property key.</param>
        /// <returns>The property value or null if it does not exist.</returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is null.</exception>
        public string GetProperty(string key)
        {
            if (key == null)
                throw new ArgumentNullException("key");

            string value;
            properties.TryGetValue(key, out value);
            return value;
        }

        /// <summary>
        /// Adds a property and replaces it if it already exists.
        /// </summary>
        /// <param name="key">The property key.</param>
        /// <param name="value">The property value.</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> or <paramref name="value"/> is null.</exception>
        public void SetProperty(string key, string value)
        {
            if (key == null)
                throw new ArgumentNullException("key");
            if (value == null)
                throw new ArgumentNullException("value");

            properties[key] = value;
        }

        /// <summary>
        /// Adds a child.
        /// </summary>
        /// <param name="key">The child key.</param>
        /// <param name="child">The child descriptor.</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> or <paramref name="child"/> is null.</exception>
        public void AddChild(string key, Descriptor child)
        {
            if (key == null)
                throw new ArgumentNullException("key");
            if (child == null)
                throw new ArgumentNullException("child");

            children.Add(new KeyValuePair<string, Descriptor>(key, child));
        }

        /// <summary>
        /// Removes a child.
        /// </summary>
        /// <param name="key">The child key.</param>
        /// <param name="child">The child descriptor.</param>
        /// <returns>True if the child previously existed and was removed.</returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> or <paramref name="child"/> is null.</exception>
        public bool RemoveChild(string key, Descriptor child)
        {
            if (key == null)
                throw new ArgumentNullException("key");
            if (child == null)
                throw new ArgumentNullException("child");

            return children.Remove(new KeyValuePair<string, Descriptor>(key, child));
        }

        /// <summary>
        /// Removes all children.
        /// </summary>
        public void ClearChildren()
        {
            children.Clear();
        }

        /// <summary>
        /// Gets the list of all children with a particular key or an empty list if none.
        /// </summary>
        /// <param name="key">The child key.</param>
        /// <returns>The list of all children with a particular key or an empty list if none.</returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is null.</exception>
        public IList<Descriptor> GetChildren(string key)
        {
            if (key == null)
                throw new ArgumentNullException("key");

            var childrenWithKey = new List<Descriptor>();
            foreach (KeyValuePair<string, Descriptor> pair in children)
                if (pair.Key == key)
                    childrenWithKey.Add(pair.Value);

            return childrenWithKey;
        }

        /// <summary>
        /// Loads a descriptor from a file.
        /// </summary>
        /// <param name="file">The file.</param>
        /// <returns>The descriptor.</returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="file"/> is null.</exception>
        /// <exception cref="IOException">Thrown if the file cannot be read.</exception>
        public static Descriptor Load(FileInfo file)
        {
            if (file == null)
                throw new ArgumentNullException("file");

            using (Stream stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                var settings = new XmlReaderSettings()
                {
                };

                using (XmlReader xmlReader = XmlReader.Create(stream, settings))
                {
                    var xmlSerializer = new XmlSerializer(typeof(DescriptorRoot));
                    var descriptorRoot = (DescriptorRoot) xmlSerializer.Deserialize(xmlReader);
                    return descriptorRoot.ToDescriptor();
                }
            }
        }

        /// <summary>
        /// Saves this descriptor to a file.
        /// </summary>
        /// <param name="file">The file.</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="file"/> is null.</exception>
        /// <exception cref="IOException">Thrown if the file cannot be written.</exception>
        public void Save(FileInfo file)
        {
            if (file == null)
                throw new ArgumentNullException("file");

            using (Stream stream = file.Open(FileMode.Create, FileAccess.Write, FileShare.None))
            {
                var settings = new XmlWriterSettings()
                {
                    Indent = true,
                    IndentChars = "  "
                };

                using (XmlWriter xmlWriter = XmlWriter.Create(stream, settings))
                {
                    var xmlSerializer = new XmlSerializer(typeof(DescriptorRoot));
                    var descriptorRoot = new DescriptorRoot(this);
                    xmlSerializer.Serialize(xmlWriter, descriptorRoot);
                }
            }
        }
    }
}
